简体   繁体   English

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

[英]Correct handling / cleanup / etc of CADisplayLink in Swift custom animation?

Consider this trivial sync animation using CADisplayLink , 使用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. 在(A :),即使link不为null,也可能无法从运行循环中删除它。 (For example, someone may have initialized it with link = link:CADisplayLink() - try it for a crash.) (例如,某人可能已使用link = link:CADisplayLink()对其进行初始化 - 尝试进行崩溃。)

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

Finally in (C:) if you want to break the anim ... I got depressed and have no clue what is best. 最后在(C :)如果你想打破这个目标...我感到沮丧并且不知道什么是最好的。

And really the code at A: and B: should be the same call right, kind of a clean-up call. 实际上A:和B:的代码应该是相同的呼叫权限,是一种清理呼叫。

Here's a simple example showing how I'd go about implementing a CADisplayLink (in Swift 3): 这是一个简单的例子,展示了我如何实现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
  }
}

Points to note: 注意事项:

  • We're using 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. 我们在这里使用nil来表示显示链接未运行的状态 - 因为没有简单的方法从无效的显示链接获取此信息。
  • Instead of using removeFromRunLoop() , we're using invalidate() , which will not crash if the display link hasn't already been added to a run-loop. 我们使用的是invalidate() ,而不是使用removeFromRunLoop() ,如果显示链接尚未添加到运行循环中,则不会崩溃。 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. 但是,这种情况绝不应该首先出现 - 因为我们总是在创建后立即将显示链接添加到运行循环中。
  • We've made the displayLink private in order to prevent outside classes from putting it in an unexpected state (eg invalidating it but not setting it to nil ). 我们将displayLink私有,以防止外部类将其置于意外状态(例如使其无效但不将其设置为nil )。
  • We have a single 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. 我们有一个stopDisplayLink()方法,它既使显示链接无效(如果它是非零)并将其设置为nil - 而不是复制和粘贴此逻辑。
  • We're not setting paused to true before invalidating the display link, as this is redundant. 在使显示链接无效之前,我们没有将paused设置为true ,因为这是多余的。
  • Instead of force unwrapping the displayLink after checking for non-nil, we're using optional chaining eg displayLink?.invalidate() (which will call invalidate() if the display link isn't nil). 在检查displayLink之后,我们使用可选链接,例如displayLink?.invalidate() (如果显示链接不是nil则调用invalidate() ,而不是强制解displayLink 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. 虽然力量展开在你给定的情况下可能是“安全的”(因为你正在检查零) - 在未来的重构时它可能是不安全的,因为你可能会重新构造你的逻辑而不考虑它对力量展开的影响。
  • We're clamping the 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. 我们将elapsed时间钳制到动画持续时间,以确保后面的动画逻辑不会产生超出预期范围的值。
  • Our update method displayLinkDidFire(_:) takes a single argument of type CADisplayLink , as required by the documentation . 我们的更新方法displayLinkDidFire(_:) 根据文档的要求采用 CADisplayLink类型的单个参数。

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) **(链接到本答案底部的演示项目 - 更新:演示项目源代码现已更新为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. 我目前正在使用这种方法在游戏中同时围绕屏幕制作约60个视图。

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. 初始化时,它将自动启动我们的显示链接,并在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 如果您想查看完整的演示项目,可以从GitHub下载: CADisplayLink演示项目

The above is the best example for how to use CADisplayLink with efficiency. 以上是如何有效使用CADisplayLink的最佳示例。 Thanks to @Fattie and @digitalHound 感谢@Fattie和@digitalHound

I could not resist adding my use of CADisplayLink and DisplayUpdater classes by 'digitalHound' in PdfViewer using WKWebView. 我无法抗拒使用WKWebView在PdfViewer中使用'digitalHound'添加我对CADisplayLink和DisplayUpdater类的使用。 My requirement was to continue auto scroll the pdf at the user selectable speed. 我的要求是继续以用户可选择的速度自动滚动pdf。

May be the answer here is not correct place, but I intent to show usage of CADisplayLink here. 可能这里答案不正确的地方,但我打算在这里展示CADisplayLink的用法。 ( for others like me, who can implement their requirement. ) (对于像我这样的人,他们可以实现他们的要求。)

//
//  PdfViewController.swift
//

import UIKit
import WebKit

class PdfViewController: UIViewController, DisplayUpdateReceiver {

    @IBOutlet var mySpeedScrollSlider: UISlider!    // UISlider in storyboard

    var displayLinker: DisplayUpdateNotifier?

    var myPdfFileName = ""                          
    var myPdfFolderPath = ""
    var myViewTitle = "Pdf View"
    var myCanAnimate = false
    var mySlowSkip = 0.0

    // 0.125<=slow, 0.25=normal, 0.5=fast, 0.75>=faster
    var cuScrollSpeed = 0.25

    fileprivate var myPdfWKWebView = WKWebView(frame: CGRect.zero)

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        self.title = myViewTitle
        let leftItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(PdfViewController.PdfBackClick))
        navigationItem.leftBarButtonItem = leftItem

        self.view.backgroundColor = UIColor.white.cgColor
        mySpeedScrollSlider.minimumValue = 0.05
        mySpeedScrollSlider.maximumValue = 4.0
        mySpeedScrollSlider.isContinuous = true
        mySpeedScrollSlider.addTarget(self, action: #selector(PdfViewController.updateSlider), for: [.valueChanged]) 
        mySpeedScrollSlider.setValue(Float(cuScrollSpeed), animated: false)
        mySpeedScrollSlider.backgroundColor = UIColor.white.cgColor

        self.configureWebView()
        let folderUrl = URL(fileURLWithPath: myPdfFolderPath)
        let url = URL(fileURLWithPath: myPdfFolderPath + myPdfFileName)
        myPdfWKWebView.loadFileURL(url, allowingReadAccessTo: folderUrl)
    }

    //MARK: - Button Action

    @objc func PdfBackClick()
    {
        _ = self.navigationController?.popViewController(animated: true)
    }

    @objc func updateSlider()
    {
        if ( mySpeedScrollSlider.value <= mySpeedScrollSlider.minimumValue ) {
            myCanAnimate = false
        } else {
            myCanAnimate = true
        }
        cuScrollSpeed = Double(mySpeedScrollSlider.value)
    }

    fileprivate func configureWebView() {
        myPdfWKWebView.frame = view.bounds
        myPdfWKWebView.translatesAutoresizingMaskIntoConstraints = false
        myPdfWKWebView.navigationDelegate = self
        myPdfWKWebView.isMultipleTouchEnabled = true
        myPdfWKWebView.scrollView.alwaysBounceVertical = true
        myPdfWKWebView.layer.backgroundColor = UIColor.red.cgColor //test
        view.addSubview(myPdfWKWebView)
        myPdfWKWebView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor ).isActive = true
        myPdfWKWebView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        myPdfWKWebView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        myPdfWKWebView.bottomAnchor.constraint(equalTo: mySpeedScrollSlider.topAnchor).isActive = true
    }

    //MARK: - DisplayUpdateReceiver delegate

    func displayWillUpdate(deltaTime: CFTimeInterval) {

        guard myCanAnimate == true else {
            return
        }

        var maxSpeed = 0.0

        if cuScrollSpeed < 0.5 {
            if mySlowSkip > 0.25 {
                mySlowSkip = 0.0
            } else {
                mySlowSkip += cuScrollSpeed
                return
            }
            maxSpeed = 0.5
        } else {
            maxSpeed = cuScrollSpeed
        }

        let scrollViewHeight = self.myPdfWKWebView.scrollView.frame.size.height
        let scrollContentSizeHeight = self.myPdfWKWebView.scrollView.contentSize.height
        let scrollOffset = self.myPdfWKWebView.scrollView.contentOffset.y
        let xOffset = self.myPdfWKWebView.scrollView.contentOffset.x

        if (scrollOffset + scrollViewHeight >= scrollContentSizeHeight)
        {
            return
        }

        let newYOffset = CGFloat( max( min( deltaTime , 1 ), maxSpeed ) )
        self.myPdfWKWebView.scrollView.setContentOffset(CGPoint(x: xOffset, y: scrollOffset+newYOffset), animated: false)
    }

}

extension PdfViewController: WKNavigationDelegate {
    // MARK: - WKNavigationDelegate
    public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        //print("didStartProvisionalNavigation")
    }

    public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        //print("didFinish")
        displayLinker = DisplayUpdateNotifier.init(listener: self)
        myCanAnimate = true
    }

    public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        //print("didFailProvisionalNavigation error:\(error)")
    }

    public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        //print("didFail")
    }

}

Example Calling from another view is as under. 示例从另一个视图调用如下。

To load the PDF file from Document folder. 从Document文件夹加载PDF文件。

func callPdfViewController( theFileName:String, theFileParentPath:String){
    if ( !theFileName.isEmpty && !theFileParentPath.isEmpty ) {
        let pdfViewController = self.storyboard!.instantiateViewController(withIdentifier: "PdfViewController") as? PdfViewController
        pdfViewController?.myPdfFileName = theFileName
        pdfViewController?.myPdfFolderPath = theFileParentPath
        self.navigationController!.pushViewController(pdfViewController!, animated: true)
    } else {
        // Show error.
    }
}

This example may be 'modified' to load web page and auto-scroll them at user selected speed. 可以“修改”此示例以加载网页并以用户选择的速度自动滚动它们。

Regards 问候

Sanjay. 桑杰。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM