[英]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: 注意事项:
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
来表示显示链接未运行的状态 - 因为没有简单的方法从无效的显示链接获取此信息。 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. 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
)。 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
- 而不是复制和粘贴此逻辑。 paused
to true
before invalidating the display link, as this is redundant. paused
设置为true
,因为这是多余的。 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. 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
时间钳制到动画持续时间,以确保后面的动画逻辑不会产生超出预期范围的值。 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.